2 // ShareTableViewDataSource.swift
5 // Created by Claudio Cambra on 27/2/24.
11 import NextcloudFileProviderKit
12 import NextcloudCapabilitiesKit
15 class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDelegate {
16 private let shareItemViewIdentifier = NSUserInterfaceItemIdentifier("ShareTableItemView")
17 private let shareItemViewNib = NSNib(nibNamed: "ShareTableItemView", bundle: nil)
18 private let reattemptInterval: TimeInterval = 3.0
20 var uiDelegate: ShareViewDataSourceUIDelegate?
21 var sharesTableView: NSTableView? {
23 sharesTableView?.register(shareItemViewNib, forIdentifier: shareItemViewIdentifier)
24 sharesTableView?.rowHeight = 42.0 // Height of view in ShareTableItemView XIB
25 sharesTableView?.dataSource = self
26 sharesTableView?.delegate = self
27 sharesTableView?.reloadData()
30 var capabilities: Capabilities?
31 var itemMetadata: NKFile?
33 private(set) var kit: NextcloudKit?
34 private(set) var itemURL: URL?
35 private(set) var itemServerRelativePath: String?
36 private(set) var shares: [NKShare] = [] {
37 didSet { Task { @MainActor in sharesTableView?.reloadData() } }
39 private var account: Account? {
41 guard let account = account else { return }
44 user: account.username,
45 userId: account.username,
46 password: account.password,
47 urlBase: account.serverUrl
52 func loadItem(url: URL) {
53 itemServerRelativePath = nil
61 DispatchQueue.main.async {
62 Timer.scheduledTimer(withTimeInterval: self.reattemptInterval, repeats: false) { _ in
63 Task { await self.reload() }
69 guard let itemURL else {
70 presentError("No item URL, cannot reload data!")
73 guard let itemIdentifier = await withCheckedContinuation({
74 (continuation: CheckedContinuation<NSFileProviderItemIdentifier?, Never>) -> Void in
75 NSFileProviderManager.getIdentifierForUserVisibleFile(
77 ) { identifier, domainIdentifier, error in
78 defer { continuation.resume(returning: identifier) }
79 guard error == nil else {
80 self.presentError("No item with identifier: \(error.debugDescription)")
85 presentError("Could not get identifier for item, no shares can be acquired.")
90 let connection = try await serviceConnection(url: itemURL, interruptionHandler: {
91 Logger.sharesDataSource.error("Service connection interrupted")
93 guard let serverPath = await connection.itemServerPath(identifier: itemIdentifier),
94 let credentials = await connection.credentials() as? Dictionary<String, String>,
95 let convertedAccount = Account(dictionary: credentials),
96 !convertedAccount.password.isEmpty
98 presentError("Failed to get details from File Provider Extension. Retrying.")
102 let serverPathString = serverPath as String
103 itemServerRelativePath = serverPathString
104 account = convertedAccount
105 await sharesTableView?.deselectAll(self)
106 capabilities = await fetchCapabilities()
107 guard capabilities != nil else { return }
108 guard capabilities?.filesSharing?.apiEnabled == true else {
109 presentError("Server does not support shares.")
113 presentError("NextcloudKit instance is unavailable, cannot reload data!")
116 itemMetadata = await fetchItemMetadata(itemRelativePath: serverPathString, kit: kit)
117 guard itemMetadata?.permissions.contains("R") == true else {
118 presentError("This file cannot be shared.")
121 shares = await fetch(
122 itemIdentifier: itemIdentifier, itemRelativePath: serverPathString
125 presentError("Could not reload data: \(error), will try again.")
131 itemIdentifier: NSFileProviderItemIdentifier, itemRelativePath: String
132 ) async -> [NKShare] {
133 Task { @MainActor in uiDelegate?.fetchStarted() }
134 defer { Task { @MainActor in uiDelegate?.fetchFinished() } }
136 let rawIdentifier = itemIdentifier.rawValue
137 Logger.sharesDataSource.info("Fetching shares for item \(rawIdentifier, privacy: .public)")
139 guard let kit = kit else {
140 self.presentError("NextcloudKit instance is unavailable, cannot fetch shares!")
144 let parameter = NKShareParameter(path: itemRelativePath)
146 return await withCheckedContinuation { continuation in
147 kit.readShares(parameters: parameter) { account, shares, data, error in
148 let shareCount = shares?.count ?? 0
149 Logger.sharesDataSource.info("Received \(shareCount, privacy: .public) shares")
150 defer { continuation.resume(returning: shares ?? []) }
151 guard error == .success else {
152 self.presentError("Error fetching shares: \(error.errorDescription)")
159 private func fetchCapabilities() async -> Capabilities? {
160 return await withCheckedContinuation { continuation in
161 kit?.getCapabilities { account, capabilitiesJson, error in
162 guard error == .success, let capabilitiesJson = capabilitiesJson else {
163 self.presentError("Error getting server caps: \(error.errorDescription)")
164 continuation.resume(returning: nil)
167 Logger.sharesDataSource.info("Successfully retrieved server share capabilities")
168 continuation.resume(returning: Capabilities(data: capabilitiesJson))
173 private func presentError(_ errorString: String) {
174 Logger.sharesDataSource.error("\(errorString, privacy: .public)")
175 Task { @MainActor in self.uiDelegate?.showError(errorString) }
178 // MARK: - NSTableViewDataSource protocol methods
180 @objc func numberOfRows(in tableView: NSTableView) -> Int {
184 // MARK: - NSTableViewDelegate protocol methods
186 @objc func tableView(
187 _ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int
189 let share = shares[row]
190 guard let view = tableView.makeView(
191 withIdentifier: shareItemViewIdentifier, owner: self
192 ) as? ShareTableItemView else {
193 Logger.sharesDataSource.error("Acquired item view from table is not a share item view!")
200 @objc func tableViewSelectionDidChange(_ notification: Notification) {
201 guard let selectedRow = sharesTableView?.selectedRow, selectedRow >= 0 else {
202 Task { @MainActor in uiDelegate?.hideOptions(self) }
205 let share = shares[selectedRow]
206 Task { @MainActor in uiDelegate?.showOptions(share: share) }